
package redeS.arquitetura;

import java.util.ArrayList;
import peersim.cdsim.CDState;
import peersim.config.Configuration;
import peersim.core.CommonState;
import peersim.core.Control;
import peersim.core.Network;
import peersim.core.Node;
import redeS.visualizacao.P2PGraph;
import redeS.visualizacao.P2PObserver;
import redeS.visualizacao.P2PObserverConstants;

/**
 * Classe de controle da arquitetura Gnutella.
 * Essa classe é responsável por aumentar e diminuir o tamanho da rede.
 * @author thiago-pc
 */
public class GnutellaControl implements Control {
	
	/**
	 * O parâmetro max_variation representa o máximo do módulo da variação do número de
	 * nós da rede a cada ciclo.
	 */
	public static final String PAR_MXVAR = "max_variation";
	private int maxVariation;
	private final int defaultMaxVariation = 0;
	
	/**
	 * O parâmetro node_conections representa o número de ligações que cada nó, ao entrar
	 * na rede, tentará realizar com seus vizinhos.
	 */
	public static final String PAR_NDCON = "node_conections";
	private int nodeConections;
	private final int defaultNodeConections = 2;
	
	/**
	 * O parâmetro frequency representa o número de vezes que o método execute irá rodar:</br>
	 * 1 é a maior frequência: executa a cada ciclo</br>
	 * para frequency = n: executa a cada n ciclos
	 */
	public static final String PAR_FREQ = "frequency";
	private int frequency;
	private final int defaultFrequency = 1;
	
	// O número do protocolo (é calculado internamente no simulador).
	private int pid = -1;
	
	private int contFrequency;
	
	public GnutellaControl(String prefix){
		// Pega o valor dos três parâmetros do arquivo de configuração. Se o arquivo de
		// configuração não tiver esses parâmetros eles serão setados com o valor padrão
		// (veja o valor default na declaração da variável).
		maxVariation = Configuration.getInt(prefix + "." + PAR_MXVAR,defaultMaxVariation);
		maxVariation = Math.abs(maxVariation);
		nodeConections = Configuration.getInt(prefix + "." + PAR_NDCON,defaultNodeConections);
		frequency = Configuration.getInt(prefix + "." + PAR_FREQ,defaultFrequency);
		if(frequency<=0) frequency = defaultFrequency;
		contFrequency = 0;
		
		if(Configuration.contains(prefix + ".protocol") ){
			pid = Configuration.getPid(prefix + ".protocol");
		}
		if(pid<0){
			System.err.println("Especifique a string identificadora do protocolo Gnutella");
			System.exit(1);
		}
		
		// /*******************/ Classe observadora /*******************/
			P2PObserver.setData(P2PObserverConstants.TITLE, null, "Arquitetura Gnutella " +
				"inicializada com " + Network.size() + " nós");		
			P2PObserver.setData(P2PObserverConstants.TITLE, null, "Controle da Rede Gnutella:\n\t" +
				"Executou a cada " + frequency + " ciclos.\n\tModificou o tamanho da rede em " +
				"até " + maxVariation + " nós a cada ciclo executado.\n\tPromoveu, no " +
				"mínimo, " + nodeConections + " vizinhos por nó");		
			P2PObserver.setData(P2PObserverConstants.VARIATION, (float)Network.size(), null);
			P2PObserver.setData(P2PObserverConstants.MAXSIZE, (float)Network.size(), null);
			P2PObserver.setData(P2PObserverConstants.MINSIZE, (float)Network.size(), null);
		// /************************************************************/
	}

	/**
	 * A cada ciclo o tamanho da rede pode aumentar ou diminuir. Essa variação é limitada
	 * por um número e esse método retorna esse número.
	 * @return int maxVariation
	 */
	public int getSizeVariation() {
		return maxVariation;
	}
	
	/**
	 * Nesse caso esse método é responsável pela escalabilidade da arquitetura Gnutella.
	 * Ele é chamado pelo PeerSim a cada ciclo alterando o número de nós na rede, para mais
	 * ou para menos. O módulo desse número é o parâmetro size_variation.
	 */
	@Override
	public boolean execute() {
		
		int numberOfNodes = 0;
		if(contFrequency % frequency == 0){
			numberOfNodes = CommonState.r.nextInt() % maxVariation;
			if(numberOfNodes > 0){
				insertNodes(numberOfNodes);
				System.out.println(this.getClass().getSimpleName() + ": inseriu " +
						numberOfNodes + " nós na rede.");
			}
			else if(numberOfNodes < 0){
				removeNodes(numberOfNodes * -1);
				System.out.println(this.getClass().getSimpleName() + ": removeu " +
						(numberOfNodes * -1) + " nós na rede.");
			}
		}
		contFrequency++;
		
		// /*******************/ Classe observadora /*******************/
		P2PObserver.setData(P2PObserverConstants.MAXSIZE, (float)Network.size(), null);
		P2PObserver.setData(P2PObserverConstants.MINSIZE, (float)Network.size(), null);
		if(numberOfNodes > 0){
			P2PObserver.setData(P2PObserverConstants.INNODES, (float)numberOfNodes, null);
		}
		else if(numberOfNodes < 0){
			P2PObserver.setData(P2PObserverConstants.OUTNODES, (float)numberOfNodes * -1, null);
		}
		if(CDState.getCycle() == CommonState.getEndTime() - 2){
			P2PObserver.setData(P2PObserverConstants.VARIATION, (float)Network.size(), null);
			for(int i=0; i<Network.size(); i++){
				float t = (float)((GnutellaProtocol)Network.get(i).getProtocol(pid)).degree();
				P2PObserver.setData(P2PObserverConstants.NEIGHBOURS, t, null);
				// P2PObserver.setData(P2PObserverConstants.MAXNEIGHBOURS, t, null);
			}
		}
		// /************************************************************/
		
		return false;
	}
	
	
	// Retira nós da rede.
	private void removeNodes(int numberOfNodes){
		Node remNode, neighbor;
		GnutellaProtocol protRemNode, protNeighbour;
		
		for(int i=0; i<numberOfNodes; i++){
			// Pega um nó qualquer da rede e o protocolo dele.
			remNode = Network.get(Math.abs(CommonState.r.nextInt() % Network.size()));
			protRemNode = (GnutellaProtocol)remNode.getProtocol(pid);
			
			// Pega cada vizinho e o protocolo dele.
			for(int j=0; j<protRemNode.degree(); j++){
				neighbor = protRemNode.getNeighbor(j);
				protNeighbour = (GnutellaProtocol)neighbor.getProtocol(pid);
				// Remove a ligação do vizinho com o nó atual.
				protNeighbour.removeNeighbor(remNode);
			}
			// Depois remove o nó (e as ligações que estão no protocolo dele vão juntas nessa!).
			Network.remove(remNode.getIndex());
		}
	}
	
	// Insere nós na rede.
	private void insertNodes(int numberOfNodes){
		Node addNode, neighbor;
		GnutellaProtocol protAddNode, protNeighbor;
		
		P2PGraph g = new P2PGraph(pid);
		ArrayList<int[]> comp = null;
		
		if(!g.isFullyConected()){
			comp = g.getNotConectedComponts(nodeConections,nodeConections);
		}
		
		for(int i=0; i<numberOfNodes; i++){
			addNode = (Node) Network.prototype.clone();
			protAddNode = (GnutellaProtocol) addNode.getProtocol(pid);
			Network.add(addNode);
			
			for(int j=0; j<nodeConections; j++){
				
				// Se o grafo for completamente conectado é escolhido um nó aleatoriamente.
				if(comp==null){
					neighbor = Network.get(Math.abs(CommonState.r.nextInt() % Network.size()));
				}
				
				// Se não for completamente conectado é escolhido um nó aleatoriamente de
				// forma a tentar resolver o problema.
				else{
					neighbor = Network.get(comp.get(j)[j]);					
				}
				
				protNeighbor = (GnutellaProtocol) neighbor.getProtocol(pid);
				protAddNode.addNeighbor(neighbor);
				protNeighbor.addNeighbor(addNode);
			}
		}
		
		// Ao final faz uma varredura na rede a procura de nós com poucas ou nenhuma
		// ligações. Fragmentos assim geralmente são deixados pelo removeNodes(int)
		for(int i=0; i<g.size(); i++){
			for(int j=g.degree(i); j<nodeConections; j++){
				addNode = Network.get(i);
				protAddNode = (GnutellaProtocol) addNode.getProtocol(pid);
				do{
					neighbor = Network.get(Math.abs(CommonState.r.nextInt() % Network.size()));
					protNeighbor = (GnutellaProtocol) neighbor.getProtocol(pid);
				}while(protAddNode.contains(neighbor));
				protAddNode.addNeighbor(neighbor);
				protNeighbor.addNeighbor(addNode);
			}
		}
	}

}
